Skip to content

feat(threading): JWZ conversation view#1188

Open
mvanhorn wants to merge 4 commits intofloatpane:masterfrom
mvanhorn:feature/509-threaded-conversation-view
Open

feat(threading): JWZ conversation view#1188
mvanhorn wants to merge 4 commits intofloatpane:masterfrom
mvanhorn:feature/509-threaded-conversation-view

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented Apr 28, 2026

What?

  • Add internal/threading package implementing the Jamie Zawinski threading algorithm against Message-ID / In-Reply-To / References headers, with subject-fallback grouping for orphans
  • Carry MessageID, InReplyTo, and References through fetcher, the IMAP/JMAP/POP3 backends, the on-disk email cache, the daemon RPC types, and the inbox model so threading works against cached headers without server round-trips
  • Inbox renders threaded mode with one row per thread root, showing the count and last-sender; Enter toggles expand/collapse; expanded children render indented with markers
  • T keybind toggles flat vs threaded for the current folder; the per-folder mode persists via folder_cache.go
  • Subject canonicalization handles Re:, Fwd:, Fw:, AW:, WG:, Tr: (lowercased, stripped repeatedly so Re: Re: Foo -> foo)
  • Tests cover: 3-message chains, forks, missing-parent placeholders, subject-fallback grouping, empty References, deterministic ordering across repeated Build() calls
  • VHS demo (screenshots/cmd/threading_demo + screenshots/threading_demo.tape): flat (5 emails) → threaded (3 rows with (3) count on the root) → expanded (5 rows with on children) → collapsed → flat

demo

Simulated demo (Remotion) — matcha theme, scripted UI, not a live capture.

Why?

This is the maintainer's spec from issue #509 and the more detailed #1130:

"Group emails into conversation threads using In-Reply-To and References headers (RFC 5322). Display threads as collapsible groups in the inbox, showing the latest message and a count of messages in the thread."

"Build threads with the Jamie Zawinski algorithm (the one Thunderbird uses) so we don't have to rely on X-GM-THRID. Threading should be done client-side from the cached header set so it works across providers."

The framing in #1130 is the user-visible argument: "Showing each reply as a separate inbox row is how Mutt looked in 1999. Modern terminal clients (aerc, himalaya) all thread."

The launch threads on r/coolgithubprojects + r/CLI + r/selfhosted (cumulative 161 upvotes, 32 comments) consistently flagged conversation grouping as the gap users notice first when comparing matcha to gmail/superhuman/aerc.

Notes

  • Touches main.go (alongside in-flight chore: refactor main.go #845 and feat: offline mode initial commit #686). Conflicts should be mechanical - the threading wiring in main.go is small (cache-conversion paths to carry References/InReplyTo). Happy to rebase or stack PRs.
  • Ordering ties in JWZ are broken on EmailID so Build() is deterministic across runs.
  • The implementation deliberately avoids X-GM-THRID and IMAP THREAD (RFC 5256) per the spec - threading is purely client-side over cached envelope data.
  • Out of scope: per-thread mark-as-read propagation rules (kept current behavior); thread-aware archive/delete (uses single-message semantics for now).

Closes #509. Addresses #1130.

This contribution was developed with AI assistance.

Adds an internal/threading package implementing the Jamie Zawinski
threading algorithm (Message-ID + In-Reply-To + References) with
subject-fallback grouping for orphans. The inbox renders one row per
thread root with a count and last sender; pressing Enter toggles
expand/collapse; the per-folder flat-vs-threaded mode persists via
folder_cache.

The MessageID/InReplyTo/References metadata is now carried through
fetcher and the IMAP/JMAP/POP3 backends, the on-disk email cache, the
daemon RPC types, and the inbox model so threading works against
cached headers without server round-trips. Per the maintainer's spec
in floatpane#509 and floatpane#1130: client-side, provider-agnostic, JWZ rather than
X-GM-THRID, deterministic ordering.

- internal/threading/jwz.go: ThreadNode, Thread, Build()
- internal/threading/subject.go: canonicalSubject()
- internal/threading/jwz_test.go: chains, forks, missing parents,
  subject-fallback grouping, deterministic ordering
- tui/inbox.go: threaded mode rendering + 'T' toggle + expand/collapse
- config/folder_cache.go: persist threaded toggle per folder
- backend/{imap,jmap,pop3}: emit MessageID/InReplyTo/References
- screenshots/cmd/threading_demo: VHS helper

Closes floatpane#509. Addresses floatpane#1130.
threading_demo.tape captures: flat (5 emails) -> threaded (3 rows with
'(3)' count on the root) -> expanded thread (5 rows with indented '↪'
markers on the children) -> collapsed -> back to flat. Rendered to
public/assets/threading_demo.gif.
- backend/jmap: Email/get now requests inReplyTo and references
  alongside messageId so JMAP-backed accounts thread by real
  References/In-Reply-To rather than falling through to subject grouping
- internal/threading/subject: add Swedish/Norwegian/Danish (SV),
  Finnish (VS), Spanish (RV), Portuguese (ENC), Dutch (Antw), Polish
  (Odp), and Italian (R/I) reply/forward prefixes
- internal/threading/jwz_test: regression coverage for SV/RV/Antw
  subject-fallback grouping
@mvanhorn mvanhorn requested a review from a team as a code owner April 28, 2026 16:04
@github-actions github-actions Bot added ci enhancement New feature or request labels Apr 28, 2026
public/assets/threading-remotion-demo.gif shows the flat inbox (5 emails),
T toggle to threaded mode (3 rows with the (3) count on the JWZ-grouped
root), Enter to expand the thread tree (showing the original + 2 replies
indented with ↪ markers). Rendered by Remotion at 1280x720, encoded via
--scale=0.6 --every-nth-frame=3.
@andrinoff
Copy link
Copy Markdown
Member

the PR has conflicts with your previous changes in #1186

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FEAT: Conversation/thread view

2 participants